/*
 * File:    Scheduler.java
 * Created: 20-Oct-2006
 *
 * by Kenji Hollis <kenji@bitgatesoftware.com>
 * Released under the Creative Commons License version 2.5
 */

package com.bitgate.util.scheduler;

import static com.bitgate.util.scheduler.SchedulerTimeContext.*;

import java.util.Calendar;
import java.util.concurrent.ConcurrentLinkedQueue;

class SchedulerTimeHistory
{
    private final int seconds, minutes, hours, days, weeks, months;

    public SchedulerTimeHistory(int seconds, int minutes, int hours, int days, int weeks, int months)
    {
		this.seconds = seconds;
		this.minutes = minutes;
		this.hours = hours;
		this.days = days;
		this.weeks = weeks;
		this.months = months;
    }

    public int getSeconds()
    {
    	return this.seconds;
    }

    public int getMinutes()
    {
    	return this.minutes;
    }

    public int getHours()
    {
    	return this.hours;
    }

    public int getDays()
    {
    	return this.days;
    }

    public int getWeeks()
    {
    	return this.weeks;
    }

    public int getMonths()
    {
    	return this.months;
    }
}

class SchedulerContainer
{
    private final SchedulerInterface si;
    private final SchedulerTimeContext stContext;
    private SchedulerTimeHistory previousTimeRun;

    public SchedulerContainer(SchedulerInterface si, SchedulerTimeContext stContext, SchedulerTimeHistory previousTimeRun)
    {
		this.si = si;
		this.stContext = stContext;
		this.previousTimeRun = previousTimeRun;
    }

    public SchedulerInterface getSchedulerInterface()
    {
    	return this.si;
    }

    public SchedulerTimeContext getSchedulerTimeContext()
    {
    	return this.stContext;
    }

    public SchedulerTimeHistory getSchedulerTimeHistory()
    {
    	return this.previousTimeRun;
    }

    public void setSchedulerTimeHistory(SchedulerTimeHistory stHistory)
    {
    	this.previousTimeRun = stHistory;
    }
}

/**
 * This class is the scheduling portion of the system - it adds the ability to run a task or set of tasks in a timely manner.
 * This system is similar to Unix crontab, which takes an object under a scheduled set of scheduling parameters, and registers
 * it into the system for processing.  This class is a singleton model for the scheduling and use of the functions.  This
 * class is intended to only be run once.
 *
 * @author Kenji Hollis &lt;kenji@bitgatesoftware.com&gt;
 * @since 0.1
 */
public class Scheduler implements Runnable
{
	// Makes these variables thread-safe since they're state-only variables.
    private volatile boolean running, stopped;
    
    private final ConcurrentLinkedQueue<SchedulerContainer> scList;
    private static final Scheduler _default = new Scheduler();

    /**
     * This is the constructor object.
     */
    private Scheduler()
    {
		this.running = true;
		this.stopped = false;
		this.scList = new ConcurrentLinkedQueue<SchedulerContainer>();
    }

    /**
     * Returns the static instance of the Scheduler object.
     *
     * @return <code>static final</code> instance of this object.
     */
    public static final Scheduler getDefault()
    {
    	return _default;
    }

    /**
     * Compares whether or not a value is between a given range.
     * 
     * @param comparator The <code>String</code> containing the range of values that a value can be part of.
     * @param value The <code>int</code> containing the value to compare.
     * @return <code>true</code> if the value falls within the range, <code>false</code> otherwise.
     */
    public boolean checkRange(String comparator, int value)
    {
		if (comparator.indexOf("-") != -1) {
		    int startRange = Integer.parseInt(comparator.substring(0, comparator.indexOf("-")));
		    int endRange = Integer.parseInt(comparator.substring(comparator.indexOf("-") + 1));
	
		    return (value >= startRange && value <= endRange);
		} else if (comparator.indexOf(",") != -1) {
		    String comparatorItems[] = comparator.split(",");
	
		    // Ugh, this is slow code.  There's no easier way around this, though.  A regexp here would be even worse.
		    for(int i = 0; i < comparatorItems.length; i++) {
				if (Integer.parseInt(comparatorItems[i]) == value) {
				    return true;
				}
		    }
	
		    return false;
		} else {
		    return (value == Integer.parseInt(comparator));
		}
    }

    /**
     * This function starts the next scheduled task or set of tasks.
     */
    private void runNextScheduledTasks()
    {
		for(SchedulerContainer i : this.scList)
		{
		    SchedulerTimeContext stContext = i.getSchedulerTimeContext();
		    SchedulerTimeHistory stHistory = i.getSchedulerTimeHistory();
		    SchedulerTimeHistory newHistory = null;
		    SchedulerInterface sInt = i.getSchedulerInterface();
		    Calendar cal = Calendar.getInstance();
		    boolean matchSecond = false, matchMinute = false, matchHour = false, matchDay = false, matchWeek = false,
			matchMonth = false, differMinute = false, differHour = false, differDay = false,
			differWeek = false, differMonth = false;
	
		    newHistory = new SchedulerTimeHistory(cal.get(Calendar.SECOND), cal.get(Calendar.MINUTE), cal.get(Calendar.HOUR_OF_DAY),
			cal.get(Calendar.DAY_OF_MONTH), cal.get(Calendar.WEEK_OF_YEAR), cal.get(Calendar.MONTH));
	
		    if (stContext.getSeconds().equals(NO_TIMES) && stContext.getMinutes().equals(NO_TIMES) && stContext.getHours().equals(NO_TIMES) &&
		    	stContext.getDays().equals(NO_TIMES) && stContext.getWeeks().equals(NO_TIMES) && stContext.getMonths().equals(NO_TIMES)) {
		    	continue;
		    }
	
		    if (!stContext.getSeconds().equals(NO_TIMES)) {
		    	matchSecond = checkRange(stContext.getSeconds(), cal.get(Calendar.SECOND));
		    } else {
		    	matchSecond = true;
		    }
	
		    if (!stContext.getMinutes().equals(NO_TIMES)) {
		    	matchMinute = checkRange(stContext.getMinutes(), cal.get(Calendar.MINUTE));
		    } else {
		    	matchMinute = true;
		    }
	
		    if (!stContext.getHours().equals(NO_TIMES)) {
		    	matchHour = checkRange(stContext.getHours(), cal.get(Calendar.HOUR_OF_DAY));
		    } else {
		    	matchHour = true;
		    }
	
		    if (!stContext.getDays().equals(NO_TIMES)) {
		    	matchDay = checkRange(stContext.getDays(), cal.get(Calendar.DAY_OF_MONTH));
		    } else {
		    	matchDay = true;
		    }
	
		    if (!stContext.getWeeks().equals(NO_TIMES)) {
		    	matchWeek = checkRange(stContext.getWeeks(), cal.get(Calendar.WEEK_OF_YEAR));
		    } else {
		    	matchWeek = true;
		    }
	
		    if (!stContext.getMonths().equals(NO_TIMES)) {
		    	matchMonth = checkRange(stContext.getMonths(), cal.get(Calendar.MONTH));
		    } else {
		    	matchMonth = true;
		    }
	
		    if (!stContext.getMinutes().equals(NO_TIMES)) {
				if (stHistory.getMinutes() != newHistory.getMinutes()) {
				    differMinute = true;
				}
		    } else {
		    	differMinute = true;
		    }
	
		    if (!stContext.getHours().equals(NO_TIMES)) {
				if (stHistory.getHours() != newHistory.getHours()) {
				    differHour = true;
				}
		    } else {
		    	differHour = true;
		    }
	
		    if (!stContext.getDays().equals(NO_TIMES)) {
				if (stHistory.getDays() != newHistory.getDays()) {
				    differDay = true;
				}
		    } else {
		    	differDay = true;
		    }
	
		    if (!stContext.getWeeks().equals(NO_TIMES)) {
				if (stHistory.getWeeks() != newHistory.getWeeks()) {
				    differWeek = true;
				}
		    } else {
		    	differWeek = true;
		    }
	
		    if (!stContext.getMonths().equals(NO_TIMES)) {
				if (stHistory.getMonths() != newHistory.getMonths()) {
				    differMonth = true;
				}
		    } else {
		    	differMonth = true;
		    }
	
		    if (matchSecond && matchMinute && matchHour && matchDay && matchWeek && matchMonth && differMinute &&
			differHour && differDay && differWeek && differMonth) {
				i.setSchedulerTimeHistory(newHistory);
				sInt.trigger();
		    }
		}
    }

    /**
     * This starts the running of the threaded object.
     */
    public void run()
    {
		while(!this.stopped) {
		    if (this.running) {
		    	runNextScheduledTasks();
		    }
	
		    try {
		    	synchronized(this) {
		    		wait(1000L);
		    	}
		    } catch(Exception e) {
		    	break;
		    }
		}
    }

    /**
     * Suspends the scheduler thread.
     */
    public void suspend()
    {
		this.running = false;
    }

    /**
     * Resumes the scheduler thread if suspended.
     */
    public void resume()
    {
		this.running = true;
    }

    /**
     * Shuts down the scheduler server.
     */
    public void shutdown()
    {
    	this.stopped = true;
    }

    /**
     * Returns an indicator as to whether or not the scheduler object is running.
     *
     * @return <code>true</code> if the scheduler is running, <code>false</code> otherwise.
     */
    public boolean isRunning()
    {
    	return this.running;
    }

    /**
     * Returns an indicator as to whether or not the scheduler is stopped.
     *
     * @return <code>true</code> if the scheduler is stopped, <code>false</code> otherwise.
     */
    public boolean isStopped()
    {
    	return this.stopped;
    }

    /**
     * Adds a task to the scheduler system.
     *
     * @param si The <code>SchedulerInterface</code> object to add.
     * @param stContext The <code>SchedulerTimeContext</code> object in which to schedule the task.
     */
    public void addTask(SchedulerInterface si, SchedulerTimeContext stContext)
    {
    	this.scList.add(new SchedulerContainer(si, stContext, new SchedulerTimeHistory(0, 0, 0, 0, 0, 0)));
    }
}
